package runner

import (
	"bufio"
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"sort"
	"strings"
	"time"

	ckeadm "ckeadm/cmd/apis"
	ckeadmv1beta1 "ckeadm/cmd/apis/v1alpha1"
	"ckeadm/constants"
	"ckeadm/util"
	"ckeadm/util/runtime"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"gopkg.in/yaml.v2"
	corev1 "k8s.io/api/core/v1"
	kuberuntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
	utilsexec "k8s.io/utils/exec"
)

var (
	scheme = kuberuntime.NewScheme()
	codec  kuberuntime.Codec
)

func init() {
	_ = ckeadmv1beta1.AddToScheme(scheme)     // registry
	_ = corev1.AddToScheme(scheme)            // corev1
	codec = GetCkeadmDefaultCodec(scheme) // codec
}

// run ansible, level = 1, 2, or 3
func AnsibleRunner(containerName, imageName, ymlFile string, backend bool, privatekey string, logLevel int, mounts ...string) error {
	var img = imageName
	var level = ""

	dockerRunTime, err := runtime.NewContainerRuntime(utilsexec.New())
	utilruntime.HandleError(err)

	if exist, err := dockerRunTime.ImageExists(img); err != nil || !exist {
		if err = util.PollImmediate(constants.PullImageInterval, constants.PullImageTimeout, pullImageCondition(dockerRunTime, img)); err != nil {
			return fmt.Errorf("timeout after %dmin pull image %s", constants.PullImageTimeout/time.Minute, img)
		}
	}

	switch logLevel {
	case 0:
		level = ""
	case 1:
		level = "-v"
	case 2:
		level = "-vv"
	case 3:
		level = "-vvv"
	}

	cmd := []string{
		SetPrivateKeyCommand(privatekey),
		fmt.Sprintf("%s -i %s %s -T %s %s -f %d", constants.AnsiblePlaybookCommand, constants.AnsibleConfig, ymlFile, constants.AnsibleTimeOut, level, constants.AnsibleForkNumber),
	}

	err = dockerRunTime.RemoveContainers([]string{containerName})
	dockerArgs := []string{
		"run",
		"--network", "host",
		"--name", containerName,
	}

	if backend == true {
		dockerArgs = append(dockerArgs, "-d")
	}

	for _, mount := range mounts {
		dockerArgs = append(dockerArgs, "-v", mount)
	}

	// #nosec
	var running = exec.Command("docker", append(dockerArgs, img, "sh", "-c", ToACommand(cmd))...)

	if backend == false {
		running.Stdout = os.Stdout
		running.Stderr = os.Stderr
	}

	return running.Run()
}

func pullImageCondition(runtime runtime.ContainerRuntime, imgname string) func() (bool, error) {
	return func() (done bool, err error) {
		logrus.Infof("trying to pull image %s ...", imgname)

		if err = runtime.PullImage(imgname); err != nil {
			logrus.Warningf("fail to pull image, err: %s", err.Error())
		}

		// if success pull docker image, return true and err with nul
		return err == nil, nil
	}
}

func SetPrivateKeyCommand(key string) string {
	return ToACommand([]string{
		fmt.Sprintf("mkdir -p /root/.ssh/"),
		fmt.Sprintf("echo %s | base64 -d > %s", key, constants.RootPrivateKey),
		fmt.Sprintf("chmod 0600 %s", constants.RootPrivateKey),
	})
}

func GetPrivateKeyFromFile(keyfile string) (key string, err error) {
	if keyfile == constants.EmptyString {
		return constants.PrivateKeyNoLine, nil
	}

	var content []byte
	if content, err = ioutil.ReadFile(filepath.Clean(keyfile)); err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(content), nil
}

func GetPrivateKeyFile(keyfile string) (file string, err error) {
	if keyfile != constants.EmptyString {
		return keyfile, nil
	}

	var content []byte
	if content, err = base64.StdEncoding.DecodeString(constants.PrivateKeyNoLine); err != nil {
		return "", err
	}

	var tmpfile = fmt.Sprintf("%s/ckev4privatekey", os.TempDir())
	if err = ioutil.WriteFile(tmpfile, content, 0600); err != nil {
		return "", err
	}

	return tmpfile, nil
}

func CreateRunerWithComponent(name, image, invFile, allFile, cmd, privatekey string, externmounts ...string) error {
	logrus.Infof("trying to run container %s, image name: %s\n", name, image)

	mounts := append([]string{
		fmt.Sprintf("%s:%s", invFile, constants.AnsibleConfig),
		fmt.Sprintf("%s:%s", allFile, constants.AnsibleAllConfig),
	}, externmounts...)
	if err := AnsibleRunner(name, image, fmt.Sprintf("%s/%s.yml", constants.CkeDeployDir, cmd), false, privatekey, constants.AnsibleLogLevel, mounts...); err != nil {
		logrus.Errorf("fail to run container %s, image name: %s, docker: %s\n", name, image, err.Error())
		return err
	}
	return nil
}

func CreateRuner(name, image, invFile, allFile, cmd, privatekey string, externmounts ...string) error {
	logrus.Infof("trying to run container %s, image name: %s\n", name, image)

	mounts := append([]string{
		fmt.Sprintf("%s:%s", invFile, constants.AnsibleConfig),
		fmt.Sprintf("%s:%s", allFile, constants.AnsibleAllConfig),
	}, externmounts...)
	if err := AnsibleRunner(name, image, fmt.Sprintf("%s/%s.yml", constants.CkeDeployDir, cmd), true, privatekey, constants.AnsibleLogLevel, mounts...); err != nil {
		logrus.Errorf("fail to run container %s, image name: %s, docker: %s\n", name, image, err.Error())
		return err
	}
	return nil
}

//get CkeCluster CkeNode CkeComponent
func GetComponents(path string) (*[]ckeadmv1beta1.CkeComponent, ckeadmv1beta1.CkeCluster, *[]ckeadmv1beta1.CkeNode, error) {
	//存储用户的ckeNode ckeCluster ckeComponent
	var ckeCluster = ckeadm.LoadConfigurationDefault()
	var ckeNodes = new([]ckeadmv1beta1.CkeNode)
	var ckeComponents = new([]ckeadmv1beta1.CkeComponent)
	err := GetCkeClusterFromFile(path, ckeCluster)
	if err != nil {
		logrus.Errorf("failed to read ckeCluster from file, %s\n", err.Error())
		return nil, *ckeCluster, nil, err
	}
	if len(ckeCluster.Spec.ClusterVips) != 0 {
		//转到原逻辑
		return nil, *ckeCluster, nil, nil
	}
	// 修改ImageManifestEnable为true，不再从配置文件中读取该参数值
	ckeCluster.Spec.ClusterParameters.ImageManifestEnabled = true

	err = GetCkeNodeFromFile(path, ckeNodes)
	if err != nil {
		logrus.Errorf("failed to read ckeNodes from file, %s\n", err.Error())
		return nil, *ckeCluster, nil, err
	}

	err = GetCkeComponentFromFile(path, ckeComponents)
	if err != nil {
		logrus.Errorf("failed to read user ckeComponents from file, %s\n", err.Error())
		return nil, *ckeCluster, ckeNodes, err
	}

	errs := ValidateCkeComponents(ckeComponents)
	if len(errs) != 0 {
		logrus.Errorf(strings.Join(errs, "\n"))
		return nil, *ckeCluster, nil, fmt.Errorf("ValidateCkeComponents failed")
	}

	//设置 ckeNodes role label
	for k, ckeNode := range *ckeNodes {
		if !hasNodeRoleLabel(getNodeRole(ckeNode.Spec.Roles), ckeNode.Spec.Labels) {
			(*ckeNodes)[k].Spec.Labels = setNodeRoleLabel(getNodeRole(ckeNode.Spec.Roles), ckeNode.Spec.Labels)
		}
	}

	for k, component := range *ckeComponents {
		(*ckeComponents)[k].Spec.ClusterName = ckeCluster.ObjectMeta.Name
		if component.Labels["baseComponent"] == "true" {
			(*ckeComponents)[k].Status.State = "Installed"
			matchNodes, notEnough := getMatchNodes(ckeNodes, component.Spec.Nodes, component.Spec.NodeSelector, component.Spec.Replicas)
			if notEnough {
				return nil, ckeadmv1beta1.CkeCluster{}, nil, fmt.Errorf("matchNodes %s are not enough for the replicas(%v) of component %s ", strings.Join(matchNodes, ","), component.Spec.Replicas, component.Name)
			}
			(*ckeComponents)[k].Status.Nodes = matchNodes
		} else if !util.ContainsStrings(component.Spec.Depend, "cke-controller") {
			(*ckeComponents)[k].Spec.Depend = util.AppendIfNotExist((*ckeComponents)[k].Spec.Depend, "cke-controller")
		}
	}

	return ckeComponents, *ckeCluster, ckeNodes, nil
}

//get CkeCluster CkeNode CkeComponent
func GetCRs(path string) (*[]ckeadmv1beta1.CkeComponent, ckeadmv1beta1.CkeCluster, *[]ckeadmv1beta1.CkeNode, error) {
	//存储用户的ckeNode ckeCluster ckeComponent
	var ckeCluster = ckeadm.LoadConfigurationDefault()
	var ckeNodes = new([]ckeadmv1beta1.CkeNode)
	var ckeComponents = new([]ckeadmv1beta1.CkeComponent)
	err := GetCkeClusterFromFile(path, ckeCluster)
	if err != nil {
		logrus.Errorf("failed to read ckeCluster from file, %s\n", err.Error())
		return nil, *ckeCluster, nil, err
	}
	if len(ckeCluster.Spec.ClusterVips) != 0 {
		//转到原逻辑
		return nil, *ckeCluster, nil, nil
	}

	err = GetCkeNodeFromFile(path, ckeNodes)
	if err != nil {
		logrus.Errorf("failed to read ckeNodes from file, %s\n", err.Error())
		return nil, *ckeCluster, nil, err
	}

	err = GetCkeComponentFromFile(path, ckeComponents)
	if err != nil {
		logrus.Errorf("failed to read user ckeComponents from file, %s\n", err.Error())
		return nil, *ckeCluster, ckeNodes, err
	}

	return ckeComponents, *ckeCluster, ckeNodes, nil
}

func ValidateCkeComponents(ckecomponents *[]ckeadmv1beta1.CkeComponent) []string {
	componentsErrs := make([]string, 0)
	for _, ckecomponent := range *ckecomponents {
		errs := make([]string, 0)
		if ckecomponent.Spec.Version == "" {
			errs = append(errs, " .spec.version is not configured")
		}
		if len(ckecomponent.Spec.Nodes) > 0 && len(ckecomponent.Spec.NodeSelector) > 0 {
			errs = append(errs, " .spec.nodes and .spec.nodeSelector are configured together")
		}
		if len(ckecomponent.Spec.Nodes) > 0 && ckecomponent.Spec.Replicas != 0 {
			errs = append(errs, " .spec.nodes and .spec.replicas are configured together")
		}
		if len(errs) != 0 {
			componentsErrs = append(componentsErrs, fmt.Sprintf("Validate ckecomponent %s failed:", ckecomponent.Name)+strings.Join(errs, ";"))
		}
	}
	return componentsErrs
}

//使用组件模板CR填充用户自定义的组件CR未指定部分字段
func CompleteComponentWithTemplate(target, template *ckeadmv1beta1.CkeComponent, completeComponentWithTemplateFailed *bool) {
	target.ObjectMeta.Namespace = template.ObjectMeta.Namespace
	if target.ObjectMeta.Labels == nil {
		target.ObjectMeta.Labels = make(map[string]string)
	}
	target.ObjectMeta.Labels["baseComponent"] = template.ObjectMeta.Labels["baseComponent"]
	if _, ok := template.ObjectMeta.Labels["containerRuntime"]; ok {
		target.ObjectMeta.Labels["containerRuntime"] = template.ObjectMeta.Labels["containerRuntime"]
	}
	if len(util.Difference(target.Spec.Depend, template.Spec.Depend)) != 0 || len(util.Difference(template.Spec.Depend, target.Spec.Depend)) != 0 {
		logrus.Errorf("the depend of ckecomponent %s is not %v ", template.Name, template.Spec.Depend)
		*completeComponentWithTemplateFailed = true
	}
	if target.Spec.Version != template.Spec.Version {
		logrus.Errorf("the version of ckecomponent %s is not %v ", template.Name, template.Spec.Version)
		*completeComponentWithTemplateFailed = true
	}
	if template.Spec.ImageTag == "" {
		if target.Spec.ImageTag != "" && target.Spec.ImageTag != template.Spec.Version {
			logrus.Errorf("the imageTag of ckecomponent %s is not %v ", template.Name, template.Spec.Version)
			*completeComponentWithTemplateFailed = true
		}
	} else if target.Spec.ImageTag != template.Spec.ImageTag {
		logrus.Errorf("the imageTag of ckecomponent %s is not %v ", template.Name, template.Spec.ImageTag)
		*completeComponentWithTemplateFailed = true
	}

	if len(util.Difference(target.Spec.NodeSelector, template.Spec.NodeSelector)) != 0 || len(util.Difference(template.Spec.NodeSelector, target.Spec.NodeSelector)) != 0 {
		logrus.Errorf("the nodeSelector of ckecomponent %s is not %v ", template.Name, template.Spec.NodeSelector)
		*completeComponentWithTemplateFailed = true
	}
	if len(util.Difference(target.Spec.Nodes, template.Spec.Nodes)) != 0 || len(util.Difference(template.Spec.Nodes, target.Spec.Nodes)) != 0 {
		logrus.Errorf("the nodes of ckecomponent %s is not %v ", template.Name, template.Spec.Nodes)
		*completeComponentWithTemplateFailed = true
	}
	if len(target.Spec.Parameters) == 0 && len(template.Spec.Parameters) != 0 {
		target.Spec.Parameters = make(map[string]string)
	}
	for k, v := range template.Spec.Parameters {
		if _, ok := target.Spec.Parameters[k]; !ok {
			target.Spec.Parameters[k] = v
		}
	}
}

func getMatchNodes(ckenodes *[]ckeadmv1beta1.CkeNode, nodes, nodeSelector []string, replicas int) ([]string, bool) {
	notEnough := false
	matchNodes := make([]string, 0)
	if len(nodes) > 0 {
		matchNodes = nodes
		return matchNodes, notEnough
	} else if len(nodeSelector) > 0 {
		for _, ckenode := range *ckenodes {
			if util.ContainsStrings(ckenode.Spec.Labels, nodeSelector...) {
				matchNodes = append(matchNodes, ckenode.Name)
			}
		}
	} else {
		for _, ckenode := range *ckenodes {
			matchNodes = append(matchNodes, ckenode.Name)
		}
	}
	sort.Strings(matchNodes)
	if replicas != 0 {
		if replicas <= len(matchNodes) {
			matchNodes = matchNodes[:replicas]
		} else {
			notEnough = true
			return matchNodes, notEnough
		}
	}
	return matchNodes, notEnough
}

func getNodeRole(roles []string) (role string) {
	// default roles is kubernetes roles
	role = constants.CkeNodeRoleWorker

	if util.ContainsStrings(roles, constants.CkeNodeRoleMaster) {
		role = constants.CkeNodeRoleMaster
	}
	return role
}

func hasNodeRoleLabel(role string, labels []string) bool {
	switch role {
	case constants.CkeNodeRoleMaster:
		return util.ContainsStrings(labels, constants.NodeMasterLabel) && util.ContainsStrings(labels, constants.NodeWorkerLabel)
	case constants.CkeNodeRoleWorker:
		return !util.ContainsStrings(labels, constants.NodeMasterLabel) && util.ContainsStrings(labels, constants.NodeWorkerLabel)
	default:
		return true
	}
}

func setNodeRoleLabel(role string, labels []string) []string {
	switch role {
	case constants.CkeNodeRoleMaster:
		return util.AppendIfNotExist(labels, constants.NodeMasterLabel, constants.NodeWorkerLabel)
	case constants.CkeNodeRoleWorker:
		return util.AppendIfNotExist(util.RemoveStrings(labels, constants.NodeMasterLabel), constants.NodeWorkerLabel)
	default:
		return labels
	}
}

//read ckeCluster from file
func GetCkeClusterFromFile(path string, cluster *ckeadmv1beta1.CkeCluster) (err error) {
	var context []byte
	var gvkSlice = new(ckeadm.GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToCluster(gvkSlice, cluster)
}

func GetCkeClusterFromYaml(yaml []byte, cluster *ckeadmv1beta1.CkeCluster) (err error) {
	var gvkSlice = new(ckeadm.GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToCluster(gvkSlice, cluster)
}

//decoded into ckeCluster
func DocumentSliceToCluster(gvkslice *ckeadm.GvkSlice, cluster *ckeadmv1beta1.CkeCluster) error {
	var clusterInit bool

	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *ckeadmv1beta1.CkeCluster:
			if clusterInit {
				return fmt.Errorf("has more than one cluster cr information")
			}
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, cluster); err != nil {
				return err
			}
			clusterInit = true
		case *ckeadmv1beta1.CkeNode:
			break
		case *ckeadmv1beta1.CkeComponent:
			break
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}

	if clusterInit == false {
		return fmt.Errorf("can't find cluster cr information")
	}
	return nil
}

//read ckeNode from file
func GetCkeNodeFromFile(path string, nodes *[]ckeadmv1beta1.CkeNode) (err error) {
	var context []byte
	var gvkSlice = new(ckeadm.GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToNode(gvkSlice, nodes)
}

func GetCkeNodeFromYaml(yaml []byte, nodes *[]ckeadmv1beta1.CkeNode) (err error) {
	var gvkSlice = new(ckeadm.GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToNode(gvkSlice, nodes)
}

//decoded into ckeNode
func DocumentSliceToNode(gvkslice *ckeadm.GvkSlice, nodes *[]ckeadmv1beta1.CkeNode) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *ckeadmv1beta1.CkeCluster:
			break
		case *ckeadmv1beta1.CkeNode:
			var ckenode ckeadmv1beta1.CkeNode
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckenode); err != nil {
				return err
			}
			*nodes = append(*nodes, ckenode)
		case *ckeadmv1beta1.CkeComponent:
			break
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}
	return nil
}

//make map[componentName][filename]
func GetCkeComponentForExplain(path string, components *[]ckeadmv1beta1.CkeComponent) (map[string]string, error) {
	var context []byte
	var gvkSlice = new(ckeadm.GvkSlice)
	var files []string
	var fileMap = make(map[string]string) //map[componentName][filename]
	var err error

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return nil, err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return nil, fmt.Errorf("%s: %s", file, err)
		}
		//here gvkSlice has only one item
		//so parse it and make map
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return nil, fmt.Errorf("%s: %s", file, err)
		}
	}

	err = DocumentSliceToComponent(gvkSlice, components)
	if err != nil {
		logrus.Errorf("DocumentSliceToComponent failed: %s", err.Error())
		return nil, err
	}

	if len(files) != len(*components) {
		logrus.Errorf("Number of file and components not match. One component in one file!")
		return nil, fmt.Errorf("Number of file and components not match. One component in one file!")
	}

	for k, file := range files {
		fileMap[(*components)[k].Name] = file
	}

	return fileMap, nil
}

//read ckeComponent from file
func GetCkeComponentFromFile(path string, components *[]ckeadmv1beta1.CkeComponent) (err error) {
	var context []byte
	var gvkSlice = new(ckeadm.GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToComponent(gvkSlice, components)
}

//read ckeComponent for list
func GetCkeComponentForList(path string, components *[]ckeadmv1beta1.CkeComponent) (err error) {
	var context []byte
	//var gvkSlice = new(ckeadm.GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		var gvkSlice = new(ckeadm.GvkSlice)
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}

		err = DocumentSliceToComponent(gvkSlice, components)
		if err != nil {
			return fmt.Errorf("DocumentSliceToComponent failed with file: %s, \n error: %s", file, err.Error())
		}
	}

	return nil
}

func GetCkeComponentFromYaml(yaml []byte, components *[]ckeadmv1beta1.CkeComponent) (err error) {
	var gvkSlice = new(ckeadm.GvkSlice)

	if err = SplitYAMLDocuments(yaml, gvkSlice); err != nil {
		return fmt.Errorf("SplitYamlFailed: %s", err.Error())
	}

	return DocumentSliceToComponent(gvkSlice, components)
}

// Decode component yaml into ckeComponent
func DocumentSliceToComponent(gvkslice *ckeadm.GvkSlice, components *[]ckeadmv1beta1.CkeComponent) error {
	if err := DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *ckeadmv1beta1.CkeCluster:
			break
		case *ckeadmv1beta1.CkeNode:
			break
		case *ckeadmv1beta1.CkeComponent:
			var ckeComponent ckeadmv1beta1.CkeComponent
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckeComponent); err != nil {
				return err
			}
			*components = append(*components, ckeComponent)
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}
	return nil
}

func GetConfigFromComponentFile(ckeComponents *[]ckeadmv1beta1.CkeComponent, ckeCluster ckeadmv1beta1.CkeCluster, ckeNodes *[]ckeadmv1beta1.CkeNode) (cfg *ckeadm.CkeAdmKubernetesConfig, err error) {
	return ckeadm.Convert_component_Configuration_To_ckeadm_Configuration(&ckeCluster, ckeNodes, ckeComponents), nil
}

func GetConfigFromFile(path string) (cfg *ckeadm.CkeAdmKubernetesConfig, err error) {
	var config = ckeadm.LoadConfigurationDefault()
	var nodes = new([]ckeadmv1beta1.CkeNode)

	if err = FetchConfigFromFile(path, config, nodes); err != nil {
		return nil, err
	}
	return ckeadm.Convert_v1beta1_Configuration_To_ckeadm_Configuration(config, nodes), nil
}

func FetchConfigFromFile(path string, config *ckeadmv1beta1.CkeCluster, nodes *[]ckeadmv1beta1.CkeNode) (err error) {
	var context []byte
	var gvkSlice = new(ckeadm.GvkSlice)
	var files []string

	if files, err = util.ListAllFilesInPath(path); err != nil {
		return err
	}

	// read files to gvk content slice
	for _, file := range files {
		if context, err = ioutil.ReadFile(filepath.Clean(file)); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
		if err = SplitYAMLDocuments(context, gvkSlice); err != nil {
			return fmt.Errorf("%s: %s", file, err)
		}
	}

	return DocumentSliceToConfiguration(gvkSlice, config, nodes)
}

func GetDockerCgroupDriver() string {
	if constants.ARCH == "mips64el" {
		return "cgroupfs"
	}
	return "systemd"
}

// write sources && back up configuration
func BackUpConfigurations(path, file string) error {
	var cluster = ckeadm.LoadConfigurationDefault()
	var nodes = new([]ckeadmv1beta1.CkeNode)
	if err := FetchConfigFromFile(path, cluster, nodes); err != nil {
		return err
	}

	var out []byte
	var err error
	var content string
	if out, err = kuberuntime.Encode(codec, cluster); err != nil {
		return err
	}
	content += fmt.Sprintf("---\n%s\n", string(out))

	for item := range *nodes {
		(*nodes)[item].Status.State = constants.RunningStatus
		(*nodes)[item].Status.Message = "this node has been init by ckeadm"

		if out, err = kuberuntime.Encode(codec, &(*nodes)[item]); err != nil {
			return err
		}
		content += fmt.Sprintf("---\n%s\n", string(out))
	}
	return util.WriteFile(file, content)
}

// write sources && back up configuration
func BackUpComponentConfigurations(ckeComponents *[]ckeadmv1beta1.CkeComponent, ckeCluster *ckeadmv1beta1.CkeCluster, ckeNodes *[]ckeadmv1beta1.CkeNode, file string) error {
	var out []byte
	var err error
	var content string
	if out, err = kuberuntime.Encode(codec, ckeCluster); err != nil {
		return err
	}
	content += fmt.Sprintf("---\n%s\n", string(out))

	for item := range *ckeNodes {
		(*ckeNodes)[item].Status.State = constants.RunningStatus
		(*ckeNodes)[item].Status.Message = "this node has been init by ckeadm"

		if out, err = kuberuntime.Encode(codec, &(*ckeNodes)[item]); err != nil {
			return err
		}
		content += fmt.Sprintf("---\n%s\n", string(out))
	}

	for item := range *ckeComponents {
		if out, err = kuberuntime.Encode(codec, &(*ckeComponents)[item]); err != nil {
			return err
		}
		content += fmt.Sprintf("---\n%s\n", string(out))
	}

	return util.WriteFile(file, content)
}

func ClusterToYaml(cluster ckeadmv1beta1.CkeCluster) ([]byte, error) {
	return kuberuntime.Encode(codec, &cluster)
}

func NodesToYaml(nodes *[]ckeadmv1beta1.CkeNode) ([]byte, error) {
	var out []byte
	var err error
	var content string
	for item := range *nodes {
		if out, err = kuberuntime.Encode(codec, &(*nodes)[item]); err != nil {
			return nil, err
		}
		content += fmt.Sprintf("---\n%s\n", string(out))
	}
	return []byte(content), nil
}

func ComponentToYaml(component ckeadmv1beta1.CkeComponent) ([]byte, error) {
	return kuberuntime.Encode(codec, &component)
}

type GroupMap map[string]string

func NewGroupMap() GroupMap {
	return make(map[string]string, 0)
}

func (no *GroupMap) GetContext() string {
	var contexts string
	for group, context := range *no {
		contexts += fmt.Sprintf("[%s]\n%s\n", group, context)

	}
	return contexts
}

func (no *GroupMap) GetGroup(group string) string {
	return (*no)[group]
}

func (no *GroupMap) AppendGroup(group string, contexts ...string) {
	for _, context := range contexts {
		(*no)[group] += context + "\n"
	}
}

func (no *GroupMap) AppendSubGroup(group string, sub ...string) {
	topic := fmt.Sprintf("%s:children", group)
	for _, context := range sub {
		(*no)[topic] += context + "\n"
	}
}

// for who names in nodes, append roles "role"
func AppendNodesRole(nodes *[]ckeadm.CkeConfigNode, role string, names ...string) {
	var nodeMap = make(map[string]*ckeadm.CkeConfigNode, 0)

	for item := range *nodes {
		nodeMap[(*nodes)[item].Name] = &(*nodes)[item]
	}

	for _, name := range names {
		if node, ok := nodeMap[name]; ok {
			node.Roles = append(node.Roles, role)
			continue
		}
		logrus.Errorf("can get nodes by name \"%s\", skip ...\n", strings.ReplaceAll(name, `"`, `\"`))
	}
}

// SplitYAMLDocuments reads the YAML bytes per-document, unmarshals the TypeMeta information from each document
// and returns a map between the GroupVersionKind of the document and the document bytes
func SplitYAMLDocuments(yamls []byte, gvkSlice *ckeadm.GvkSlice) error {
	reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewBuffer(yamls)))
	for {
		typeMetaInfo := kuberuntime.TypeMeta{}
		// Read one YAML document at a time, until io.EOF is returned
		b, err := reader.Read()
		if err == io.EOF {
			break
		} else if err != nil {
			return err
		}

		if len(strings.ReplaceAll(string(b), "\n", "")) == 0 {
			continue
		}

		// Deserialize the TypeMeta information of this byte slice
		if err := yaml.Unmarshal(b, &typeMetaInfo); err != nil {
			return err
		}

		// Require TypeMeta information to be present
		if len(typeMetaInfo.APIVersion) == 0 || len(typeMetaInfo.Kind) == 0 {
			return errors.New("invalid configuration: kind and apiVersion is mandatory information that needs to be specified in all YAML documents")
		}

		// Build a GroupVersionKind object from the deserialized TypeMeta object
		gv, err := schema.ParseGroupVersion(typeMetaInfo.APIVersion)
		if err != nil {
			return errors.Wrap(err, "unable to parse apiVersion")
		}
		gvk := gv.WithKind(typeMetaInfo.Kind)

		// Save the mapping between the gvk and the bytes that object consists of
		*gvkSlice = append(*gvkSlice, ckeadm.GVKContent{GVK: gvk, Content: b})
	}

	return nil
}

// issue: support v1 list. so you can use follow steps get v3 configuration
// ```
// # mkdir -p /root/cke-config/
// # kubectl get ckenode -A -o yaml > /root/cke-config/node.yml
// # kubectl get ckecluster -A -o yaml > /root/cke-config/cluster.yml
// # ckeadm transform -f config
// ```
func DocumentListToObjects(gvkslice *ckeadm.GvkSlice) error {
	for item, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			continue
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *corev1.List:
			var list = new(corev1.List)
			if err := kuberuntime.DecodeInto(codec, gvkcontent.Content, list); err != nil {
				return err
			}
			*gvkslice = append((*gvkslice)[:item], (*gvkslice)[item+1:]...)
			for _, object := range list.Items {
				if err := SplitYAMLDocuments(object.Raw, gvkslice); err != nil {
					return err
				}
			}
			return DocumentListToObjects(gvkslice)
		}
	}

	return nil
}

// DocumentSliceToObject takes a map between GVKs and YAML documents (as returned by SplitYAMLDocuments),
// finds needconfig, decodes it, dynamically defaults it and then validates it prior to return.
func DocumentSliceToConfiguration(gvkslice *ckeadm.GvkSlice, config *ckeadmv1beta1.CkeCluster, nodes *[]ckeadmv1beta1.CkeNode) (err error) {
	var clusterInited bool

	if err = DocumentListToObjects(gvkslice); err != nil {
		return err
	}

	for _, gvkcontent := range *gvkslice {
		if scheme.AllKnownTypes()[gvkcontent.GVK] == nil {
			return fmt.Errorf("no kind %s is registered for version %s", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
		switch reflect.New(scheme.AllKnownTypes()[gvkcontent.GVK]).Interface().(type) {
		case *ckeadmv1beta1.CkeNode:
			var ckenode ckeadmv1beta1.CkeNode
			if err = kuberuntime.DecodeInto(codec, gvkcontent.Content, &ckenode); err != nil {
				return err
			}
			if *nodes, err = AppendToNodesList(*nodes, ckenode); err != nil {
				return err
			}
		case *ckeadmv1beta1.CkeCluster:
			if clusterInited {
				return fmt.Errorf("has more than one cluster cr information")
			}
			if err = kuberuntime.DecodeInto(codec, gvkcontent.Content, config); err != nil {
				return err
			}
			clusterInited = true
		case *ckeadmv1beta1.CkeComponent:
			break
		default:
			logrus.Warningf("kind %s version %s configuration found, but ckeadm unsupported", gvkcontent.GVK.Kind, gvkcontent.GVK.GroupVersion())
		}
	}

	if clusterInited == false {
		return fmt.Errorf("can't find cluster cr information")
	}
	if len(*nodes) == 0 {
		return fmt.Errorf("can't find node cr information")
	}

	return nil
}

func AppendToNodesList(nodelist []ckeadmv1beta1.CkeNode, node ckeadmv1beta1.CkeNode) ([]ckeadmv1beta1.CkeNode, error) {
	for _, existnode := range nodelist {
		if node.Name == existnode.Name {
			return []ckeadmv1beta1.CkeNode{}, fmt.Errorf("exist the same node name, %s", node.Name)
		}
		if node.Spec.Address == existnode.Spec.Address {
			return []ckeadmv1beta1.CkeNode{}, fmt.Errorf("exist the same spec.address with node \"%s\" and \"%s\"", node.Name, existnode.Name)
		}
		if node.Spec.Address == existnode.Spec.Address {
			return []ckeadmv1beta1.CkeNode{}, fmt.Errorf("exist the same spec.address with node \"%s\" and \"%s\"", node.Name, existnode.Name)
		}
		if node.Annotations["install_net_address"] != constants.EmptyString && node.Annotations["install_net_address"] == existnode.Annotations["install_net_address"] {
			return []ckeadmv1beta1.CkeNode{}, fmt.Errorf("exist the same annotations.install_net_address with node \"%s\" and \"%s\"", node.Name, existnode.Name)
		}
	}

	return append(nodelist, node), nil
}

func GetImage(registry string, port int, name, tag string, disableArchTag bool) string {
	var head, body, end = GetRegistry(registry, port), name, tag

	// if enable archtag, add arch information for image. such as xxx-amd64
	if !disableArchTag {
		body = fmt.Sprintf("%s-%s", body, constants.ARCH)
	}

	return fmt.Sprintf("%s/%s:%s", head, body, end)
}

func GetRegistry(registry string, port int) string {
	// if has port, config registry with port as xxxx:xxxx
	if port > 0 {
		return fmt.Sprintf("%s:%d", registry, port)
	}

	return registry
}

type ckeadmcodec struct {
	kuberuntime.Encoder
	kuberuntime.Decoder
}

func NewCkeadmCodec(encoder kuberuntime.Encoder, decoder kuberuntime.Decoder) kuberuntime.Codec {
	return &ckeadmcodec{encoder, decoder}
}

func GetCkeadmDefaultCodec(scheme *kuberuntime.Scheme) kuberuntime.Codec {
	var codecfactory = serializer.NewCodecFactory(scheme)
	var infoer, _ = kuberuntime.SerializerInfoForMediaType(codecfactory.SupportedMediaTypes(), kuberuntime.ContentTypeYAML)

	return NewCkeadmCodec(infoer.Serializer, codecfactory.UniversalDecoder())
}

func NoneIfEmpty(str string) string {
	if str == constants.EmptyString {
		return "<none>"
	}

	return str
}

func ToACommand(cmds []string) (cmd string) {
	return strings.Join(cmds, " && ")
}

//4.4 ~ 4.4.1
//componentVersion x.x.*~x.x.x
func CheckVersion(componentCkeVersion, clusterVersion string) bool {
	versionArray := strings.Split(componentCkeVersion, "~")
	switch len(versionArray) {
	case 0:
		return true
	case 1:
		if (VersionCompare(versionArray[0], clusterVersion) == -1) || (VersionCompare(versionArray[0], clusterVersion) == 0) {
			return true
		}
	case 2:
		if (VersionCompare(versionArray[0], clusterVersion) == -1) || (VersionCompare(versionArray[0], clusterVersion) == 0) {
			if (VersionCompare(versionArray[1], clusterVersion) == 1) || (VersionCompare(versionArray[1], clusterVersion) == 0) {
				return true
			}
		}
		return false
	default:
		return false
	}
	return false
}

//v1 x.x.* v2 x.x.*
func VersionCompare(v1, v2 string) int {
	v1Array := strings.Split(v1, ".")
	v2Array := strings.Split(v2, ".")
	for index, digi := range v1Array {
		if index > len(v2Array)-1 {
			return 1
		}
		if digi == "*" {
			return -1
		} else if digi > v2Array[index] {
			return 1
		} else if digi < v2Array[index] {
			return -1
		} else {
			if len(v1Array)-1 == index {
				if len(v1Array) == len(v2Array) {
					return 0
				}
				return -1
			}
			continue
		}
	}
	return -1
}
